// TODO: should we make this threaded?
package pricingEngine;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Random;

import payout.OptionA;
import payout.OptionB;
import payout.StandardCall;
import payout.Payout;
import yieldCurve.Date;
import yieldCurve.YieldCurve;
import variance.BlackScholes;

public class MonteCarlo extends Engine {
	int M;
	int N;
	double deltaT;
	double r;
	private double finalPrice;
	private double finalConfidence;
	/** Constructor method for the Monte Carlo engine, usually use the benchmark option to construct
	 * 
	 * @param yieldCurve Previously-calculated yield curve from which to get the discount factor
	 * @param sigma Variance of the stock
	 * @param S0 Stock price at the current date (i.e. initial price)
	 * @param strike Strike price
	 * @param startDate start date of the option
	 * @param expiryDate expiry date of the option 
	 * @param payout the interface of payout function
	 * @param M Number of simulations to perform
	 * @param N Length of each simulation path
	 */
	public MonteCarlo(YieldCurve yieldCurve, double sigma, double S0, double strike, 
			Date startDate, Date expiryDate, Payout payout, int M, int N) {
		super(yieldCurve, sigma, S0, strike, startDate, expiryDate, payout);
		this.M = M;
		this.N = N;

		//check if today is a business day
		startDate = Date.businessDay(startDate);
		//check if the given expiration day is a business day
		deltaT = Date.getDeltaT(startDate, expiryDate) / N;
		r = yieldCurve.getContinuousCompound(deltaT*N);
		// r is an array of continuous compound rates at the time slot indicated by its index
		if(Debug.DEBUG == true) {
			System.out.println("M:"+M+",  N:"+N);
			System.out.println("today:" + startDate);
			System.out.println("expiry:" + expiryDate);
			System.out.println("deltaT:"+deltaT);
		}
	}
	
	/**
	 * inheriting the super class, add 2 extra field, M, N
	 * @param S0
	 * @param strike
	 * @param startDate
	 * @param expiryDate
	 * @param payout
	 * @param M the number of trials
	 * @param N the number of intervals (i.e. the number of layers plus one)
	 */
	public void setOptionInfo(double S0, double strike, 
			Date startDate, Date expiryDate, Payout payout, int M, int N) {
		super.setOptionInfo(S0, strike, startDate, expiryDate, payout);

		// Sanity checking
		if(M<1) {
			System.out.println("M must be a positive value.");
		}
		if(N<1) {
			System.out.println("N must be a positive value.");
		}
		this.M = M;
		this.N = N;
	}
	
	/** Experimental version of MC loop with optimization
	 *  getMean with println() removed takes 13175, 13174, 13129ms (2GHz core 2 duo, M=70000 N=500)
	 *  runLoop version takes 12296, 12290, 12423ms
	 */
	public double getOptionPrice() {
		Random ran = new Random();
		RandomNumber ourran = new RandomNumber();
	
		// the payout calculation machine
		int j; // for looping
		double s,s_neg, DSOverS, DSOverS_neg;
		double sum=0; // running total
		double sumVar = 0; // sum of payout*payout
		double sigma_root_deltaT = sigma*Math.sqrt(deltaT);
		double tempPayout;
		double randdelt;

		//max price of final layer
		double Smin_1;
		double Smin_2;
		//min price of final layer
		double Smax_1; 
		double Smax_2; 
		double variance;
		double tempPrice;
		double rdt = r*deltaT;
		double stderror;
		double price;
		double df = continuousRateToDf(r);
		double dfdf = df * df;
		double [][] randarray;
		int Miterations;
		if(Debug.RANDARRAY == true) {
			randarray = new double[N][N];
		}
		// number of iterations in main loop
		if(Debug.ANTITHETIC == true) {
			Miterations=M/2;
		} else {
			Miterations=M;
		}
		Boolean minmax_needed = false;
		if(payout.isLookback() == true) {
			minmax_needed = true;
		}
		PrintWriter priceWriter;
		try {
			if(Debug.PROFILING == true) {
				priceWriter = new PrintWriter(new BufferedWriter(new FileWriter("Mc" + payout.getClass().getName() 
							+ "-" + startDate.toString() + "-M" + M + "-N" + N 
							+ "-AN" + Debug.ANTITHETIC + "BX" + Debug.BOXMULLER + ".txt")));
			}
			for ( int i = 1 ; i <= Miterations ; i++){
				s = S0;
				Smin_1 = Double.MAX_VALUE;
				Smax_1 = 0;
	
				if(Debug.ANTITHETIC == true) {
					s_neg = S0;
					Smin_2 = Double.MAX_VALUE;
					Smax_2 = 0;
				}

				if(Debug.RANDARRAY == true && (i-1)%(N*2) == 0){
					// build a box of random numbers (next week: build a hypercube)
					for(int k=0;k<N;k++) {
						for(int m=0;m<N;m++) {
							randarray[k][m] = ran.nextGaussian() * sigma_root_deltaT;
						}
					}
				}
				
				// follow a random path of length N
				for ( j = 0 ; j < N ; j++){
					if(Debug.BOXMULLER == true) {
						if(Debug.RANDARRAY == true) {
							if((i-1)%(2*N)<N ) {
								randdelt = randarray[j][(i-1)%N];
							} else {
								randdelt = randarray[(i-1)%N][j];
							}
						} else {
							randdelt = ran.nextGaussian()*sigma_root_deltaT;
						}
					} else {
						randdelt = ourran.normalDistributedNum()*sigma_root_deltaT;
					}
					DSOverS = rdt + randdelt;
					s = s*(1+DSOverS);
					
					if(Debug.ANTITHETIC == true) {
						DSOverS_neg = rdt - randdelt; // i.e. = r.get(j)*deltaT - ran.nextGaussian()*sigma_root_deltaT;
						s_neg = s_neg*(1+DSOverS_neg);
					}
					// for option B, the max and min stock price have to be recorded for payout
					if (minmax_needed) { // TODO:  can this be moved outside the loop somehow?
						if (s > Smax_1)
							Smax_1 = s;
						if (s < Smin_1)
							Smin_1 = s;
						if(Debug.ANTITHETIC == true) {
							if (s_neg > Smax_2)
								Smax_2 = s_neg;
							if (s_neg < Smin_2)
								Smin_2 = s_neg;
						}
					}
				}
	
				// get the payout function for this iteration and add it to the running total
				tempPayout = payout.getPayout(s, strike, Smax_1, Smin_1);
				sum += tempPayout;
				sumVar += tempPayout * tempPayout;
				
				if(Debug.ANTITHETIC == true) {
					tempPayout = payout.getPayout(s, strike, Smax_2, Smin_2);
					sum += tempPayout;
					sumVar += tempPayout * tempPayout;
				}
				
				// confidence over time.
				if(Debug.PROFILING == true) {
					if(i%2500 == 2499 ) {
						int curr_i;
						if(Debug.ANTITHETIC == true) {
							curr_i = i*2;
						} else {
							curr_i = i;
						}
						tempPrice = sum / (curr_i);
						variance = (sumVar / (curr_i) - tempPrice * tempPrice) * dfdf;
						stderror = Math.sqrt(variance/curr_i) * df;
						price = tempPrice * df;
						priceWriter.println(N + "," + (curr_i+1) + "," + price + "," + stderror + "," + variance);
						if(Debug.DEBUG == true) {
							System.out.println( "price," + price +
									",variance at M=," + curr_i + ", is ," + variance + ",undiscounted price," + sum/curr_i
									+ ",stderror," + stderror + ",confidence," + 2.326 * stderror);
						}
					}
				}
			}
			finalPrice = sum / M;
			variance = (sumVar / M - finalPrice * finalPrice) * dfdf;
			finalConfidence = Math.sqrt(variance/M);
			System.out.println("Final confidence: " + finalConfidence *2.326);
			if(Debug.PROFILING == true) {
				priceWriter.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
			System.err.println("End of stream");
		}
		return finalPrice;
	}
	
	public double variance(ArrayList<Double> data_array, double mean) {
		double variance=0;
		
		// find difference from mean
		for(int i=0;i<data_array.size();i++) {
			variance += (data_array.get(i)-mean)*(data_array.get(i)-mean);
		}
		return variance / data_array.size();
	}
	
	/** Discount the average payout value back to today
	 * 
	 * @param finalPayout Final expected value
	 * @return Value discounted back to today
	 */
	public double discountPayout(double finalPayout){
		return finalPayout * continuousRateToDf(r);
	}
	
	/** Get a discount factor from the continuous rate
	 * 
	 * @param continuousRate
	 * @return Discount factor
	 */
	public double continuousRateToDf(double continuousRate){
		
		return Math.exp(-continuousRate*deltaT*N);
	}
	
	/** Unit test function for Monte Carlo object - inputs fixed values, output should be around 25.
	 * 
	 * @param args No args currently supported
	 */
	public static void main(String args[]){

		// Settings for option to be tested appear here
		Date today = new Date(2009,5,1);
		YieldCurve yieldCurve = new YieldCurve("curveDataInput.csv", "curveSpec.csv", 2, today);
		Date expiry = new Date(2011,1,22);
		int M=2000000;
		// for testing for convergence; Ninit TO Nmax STEP step are the tests we run to see the effect of changing N.
		int Ninit;
		int Nmax;
		int step;
		if(Debug.PROFILING == true) {
			Ninit=10000;
			Nmax=1000;
			step=100;
		} else {
			Ninit=100;
			Nmax=100;
			step=1;
		}
		double currTrade=89.31;
		double strikePrice = 95.0;
		double benchmarkPrice = 25.0;
	
		// find volatility
		BlackScholes bc = new BlackScholes(yieldCurve, currTrade, today, expiry, strikePrice, benchmarkPrice);
		double sigma = bc.getSigma();
		System.out.println("volatility=" + sigma);

		// Type of option goes here
		Payout call = new OptionA();
		
		for(int N=Ninit;N<=Nmax;N+=step) {
			MonteCarlo mc = new MonteCarlo(yieldCurve, sigma, currTrade, strikePrice, 
				today, expiry, call, M, N);		
			long start = System.currentTimeMillis();
			mc.getOptionPrice();		
			System.out.println("N="+N+", mean payout:"+mc.getFinalPrice());
			double payout_today = mc.discountPayout(mc.getFinalPrice());
			System.out.println("final payout discounted to today: "+payout_today+ ", with confidence: " +mc.getFinalConfidence());
			long stop = System.currentTimeMillis();
			long elapsed = stop - start;
			System.out.println("Monte Carlo took " + elapsed + "ms to complete.");
		}
	}

	public void setFinalPrice(double finalPrice) {
		this.finalPrice = finalPrice;
	}

	public double getFinalPrice() {
		return finalPrice;
	}

	public void setFinalConfidence(double finalConfidence) {
		this.finalConfidence = finalConfidence;
	}

	public double getFinalConfidence() {
		return finalConfidence;
	}

}